/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import static org.hamcrest.core.StringContains.containsString;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.search.CollapsingQParserPlugin.GroupHeadSelector;
import org.apache.solr.search.CollapsingQParserPlugin.GroupHeadSelectorType;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestCollapseQParserPlugin extends SolrTestCaseJ4 {
  @BeforeClass
  public static void beforeClass() throws Exception {
    // we need DVs on point fields to compute stats & facets
    if (Boolean.getBoolean(NUMERIC_POINTS_SYSPROP))
      System.setProperty(NUMERIC_DOCVALUES_SYSPROP, "true");
    initCore("solrconfig-collapseqparser.xml", "schema11.xml");
  }

  @Override
  @Before
  public void setUp() throws Exception {
    // if you override setUp or tearDown, you better call
    // the super classes version
    super.setUp();
    clearIndex();
    assertU(commit());
  }

  public void testMultiSort() {
    assertU(adoc("id", "1", "group_s", "group1", "test_i", "5", "test_l", "10"));
    assertU(commit());
    assertU(adoc("id", "2", "group_s", "group1", "test_i", "5", "test_l", "1000"));
    assertU(adoc("id", "3", "group_s", "group1", "test_i", "5", "test_l", "1000"));
    assertU(adoc("id", "4", "group_s", "group1", "test_i", "10", "test_l", "100"));
    //
    assertU(adoc("id", "5", "group_s", "group2", "test_i", "5", "test_l", "10", "term_s", "YYYY"));
    assertU(commit());
    assertU(adoc("id", "6", "group_s", "group2", "test_i", "5", "test_l", "1000"));
    assertU(
        adoc("id", "7", "group_s", "group2", "test_i", "5", "test_l", "1000", "term_s", "XXXX"));
    assertU(adoc("id", "8", "group_s", "group2", "test_i", "10", "test_l", "100"));
    assertU(commit());

    ModifiableSolrParams params;

    // group heads are selected using the same sort that is then applied to the final groups
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort=$sort}");
    params.add("sort", "test_i asc, test_l desc, id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='7']",
        "//result/doc[2]/str[@name='id'][.='3']");

    // group heads are selected using a complex sort, simpler sort used for final groups
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort='test_i asc, test_l desc, id_i desc'}");
    params.add("sort", "id_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='3']",
        "//result/doc[2]/str[@name='id'][.='7']");

    // diff up the sort directions, only first clause matters with our data
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort='test_i desc, test_l asc, id_i asc'}");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='8']",
        "//result/doc[2]/str[@name='id'][.='4']");

    // score, then tiebreakers; note top level sort by score ASCENDING (just for weirdness)
    params = new ModifiableSolrParams();
    params.add("q", "*:* term_s:YYYY");
    params.add(
        "fq", "{!collapse field=group_s sort='score desc, test_l desc, test_i asc, id_i asc'}");
    params.add("sort", "score asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='2']",
        "//result/doc[2]/str[@name='id'][.='5']");

    // score, then tiebreakers; note no score in top level sort/fl to check needsScores logic
    params = new ModifiableSolrParams();
    params.add("q", "*:* term_s:YYYY");
    params.add(
        "fq", "{!collapse field=group_s sort='score desc, test_l desc, test_i asc, id_i asc'}");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='2']");

    // term_s desc -- term_s is missing from many docs, and uses sortMissingLast=true
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort='term_s desc, test_l asc'}");
    params.add("sort", "id_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='1']",
        "//result/doc[2]/str[@name='id'][.='5']");

    // term_s asc -- term_s is missing from many docs, and uses sortMissingLast=true
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort='term_s asc, test_l asc'}");
    params.add("sort", "id_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='1']",
        "//result/doc[2]/str[@name='id'][.='7']");

    // collapse on int field
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=test_i sort='term_s asc, group_s asc'}");
    params.add("sort", "id_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='4']",
        "//result/doc[2]/str[@name='id'][.='7']");

    // collapse on term_s (very sparse) with nullPolicy=collapse
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq",
        "{!collapse field=term_s nullPolicy=collapse sort='test_i asc, test_l desc, id_i asc'}");
    params.add("sort", "test_l asc, id_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='2']",
        "//result/doc[3]/str[@name='id'][.='7']");

    // sort local param + elevation
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort='term_s desc, test_l asc'}");
    params.add("sort", "test_l asc");
    params.add("qt", "/elevate");
    params.add("forceElevation", "true");
    params.add("elevateIds", "4");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='4']",
        "//result/doc[2]/str[@name='id'][.='5']");
    //
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_s sort='term_s desc, test_l asc'}");
    params.add("sort", "test_l asc");
    params.add("qt", "/elevate");
    params.add("forceElevation", "true");
    params.add("elevateIds", "7");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='7']",
        "//result/doc[2]/str[@name='id'][.='1']");
  }

  @Test
  public void testStringCollapse() {
    for (final String hint : new String[] {"", " hint=" + CollapsingQParserPlugin.HINT_TOP_FC}) {
      testCollapseQueries("group_s", hint, false);
      testCollapseQueries("group_s_dv", hint, false);
    }
  }

  @Test
  public void testNumericCollapse() {
    final String hint = "";
    testCollapseQueries("group_i", hint, true);
    testCollapseQueries("group_ti_dv", hint, true);
    testCollapseQueries("group_f", hint, true);
    testCollapseQueries("group_tf_dv", hint, true);
  }

  @Test
  public void testFieldValueCollapseWithNegativeMinMax() {
    String[] doc = {
      "id", "1", "group_i", "-1000", "test_i", "5", "test_l", "-10", "test_f", "2000.32"
    };
    assertU(adoc(doc));
    assertU(commit());
    String[] doc1 = {
      "id", "2", "group_i", "-1000", "test_i", "50", "test_l", "-100", "test_f", "2000.33"
    };
    assertU(adoc(doc1));

    String[] doc2 = {"id", "3", "group_i", "-1000", "test_l", "100", "test_f", "200"};
    assertU(adoc(doc2));
    assertU(commit());
    String[] doc3 = {"id", "4", "test_i", "500", "test_l", "1000", "test_f", "2000"};
    assertU(adoc(doc3));

    String[] doc4 = {
      "id", "5", "group_i", "-1000", "test_i", "4", "test_l", "10", "test_f", "2000.31"
    };
    assertU(adoc(doc4));
    assertU(commit());
    String[] doc5 = {
      "id", "6", "group_i", "-1000", "test_i", "10", "test_l", "100", "test_f", "-2000.12"
    };
    assertU(adoc(doc5));
    assertU(commit());

    String[] doc6 = {
      "id", "7", "group_i", "-1000", "test_i", "8", "test_l", "-50", "test_f", "-100.2"
    };
    assertU(adoc(doc6));
    assertU(commit());

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_i min=test_f}");
    assertQ(req(params), "*[count(//doc)=1]", "//result/doc[1]/str[@name='id'][.='6']");

    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=group_i max=test_f}");
    assertQ(req(params), "*[count(//doc)=1]", "//result/doc[1]/str[@name='id'][.='2']");
  }

  @Test // https://issues.apache.org/jira/browse/SOLR-9494
  public void testNeedsScoreBugFixed() {
    String[] doc = {"id", "1", "group_s", "xyz", "text_ws", "hello xxx world"};
    assertU(adoc(doc));
    assertU(commit());

    ModifiableSolrParams params =
        params(
            "q", "{!surround df=text_ws} 2W(hello, world)", // a SpanQuery that matches
            "fq", "{!collapse field=group_s}", // collapse on some field
            // note: rows= whatever; doesn't matter
            "facet", "true", // facet on something
            "facet.field", "group_s");
    assertQ(req(params));
    assertQ(req(params)); // fails *second* time!
  }

  @Test
  public void testMergeBoost() throws Exception {

    Set<Integer> boosted = new HashSet<>();
    Set<Integer> results = new HashSet<>();

    for (int i = 0; i < 200; i++) {
      boosted.add(random().nextInt(1000));
    }

    for (int i = 0; i < 200; i++) {
      results.add(random().nextInt(1000));
    }

    int[] boostedArray = new int[boosted.size()];
    int[] resultsArray = new int[results.size()];

    Iterator<Integer> boostIt = boosted.iterator();
    int index = 0;
    while (boostIt.hasNext()) {
      boostedArray[index++] = boostIt.next();
    }

    Iterator<Integer> resultsIt = results.iterator();
    index = 0;
    while (resultsIt.hasNext()) {
      resultsArray[index++] = resultsIt.next();
    }

    Arrays.sort(boostedArray);
    Arrays.sort(resultsArray);

    CollapsingQParserPlugin.MergeBoost mergeBoost =
        new CollapsingQParserPlugin.MergeBoost(boostedArray);

    List<Integer> boostedResults = new ArrayList<>();

    for (int result : resultsArray) {
      if (mergeBoost.boost(result)) {
        boostedResults.add(result);
      }
    }

    List<Integer> controlResults = new ArrayList<>();

    for (int result : resultsArray) {
      if (Arrays.binarySearch(boostedArray, result) > -1) {
        controlResults.add(result);
      }
    }

    if (boostedResults.size() == controlResults.size()) {
      for (int i = 0; i < boostedResults.size(); i++) {
        if (!boostedResults.get(i).equals(controlResults.get(i))) {
          throw new Exception(
              "boosted results do not match control results, boostedResults size:"
                  + boostedResults
                  + ", controlResults size:"
                  + controlResults);
        }
      }
    } else {
      throw new Exception(
          "boosted results do not match control results, boostedResults size:"
              + boostedResults
              + ", controlResults size:"
              + controlResults);
    }
  }

  @Test
  public void testDoubleCollapse() {
    testDoubleCollapse("group_s");
    testDoubleCollapse("group_i");
  }

  /*
   * SOLR-14073
   * The double collapse causes a look ahead in the second collapse to a segment that was not visited by
   * the by finally method of the first collapse. This specific test is meant to confirm that any feature
   * that causes searches to not visit each segment (such as early query termination) doesn't break collapse.
   */
  private void testDoubleCollapse(String group) {
    String[] doc = {
      "id", "1", "term_s", "YYYY", group, "1", "test_i", "5", "test_l", "10", "test_f", "2000"
    };
    assertU(adoc(doc));
    assertU(commit());
    String[] doc1 = {
      "id", "2", "term_s", "YYYY", group, "2", "test_i", "50", "test_l", "100", "test_f", "200"
    };
    assertU(adoc(doc1));

    String[] doc2 = {
      "id", "3", "term_s", "YYYY", "test_i", "5000", "test_l", "100", "test_f", "200"
    };
    assertU(adoc(doc2));
    assertU(commit());
    String[] doc3 = {
      "id", "4", "term_s", "YYYY", "test_i", "500", "test_l", "1000", "test_f", "2000"
    };
    assertU(adoc(doc3));

    String[] doc4 = {
      "id", "5", "term_s", "YYYN", group, "2", "test_i", "4", "test_l", "10", "test_f", "2000"
    };
    assertU(adoc(doc4));
    assertU(commit());
    String[] doc5 = {
      "id", "6", "term_s", "YYYY", group, "2", "test_i", "10", "test_l", "100", "test_f", "200"
    };
    assertU(adoc(doc5));
    assertU(commit());

    String[] doc6 = {
      "id", "7", "term_s", "YYYY", group, "1", "test_i", "8", "test_l", "50", "test_f", "300"
    };
    assertU(adoc(doc6));
    assertU(commit());

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add("q", "id:(1 2 5)");
    params.add("fq", "{!collapse cost=200 field=term_s }");
    params.add("fq", "{!collapse cost=400 field=" + group + "}");

    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    assertQ(
        req(params, "indent", "on"), "*[count(//doc)=1]", "//result/doc[1]/str[@name='id'][.='2']");

    params = new ModifiableSolrParams();
    params.add("q", "id:(1 2 5)");
    params.add("fq", "{!collapse cost=200 max=test_i field=term_s }");
    params.add("fq", "{!collapse cost=400 max=test_i field=" + group + "}");

    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    assertQ(
        req(params, "indent", "on"), "*[count(//doc)=1]", "//result/doc[1]/str[@name='id'][.='2']");
  }

  private void testCollapseQueries(String group, String hint, boolean numeric) {
    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + "" + hint + "}");
    assertQ(req(params, "indent", "on"), "*[count(//doc)=0]");

    String[] doc = {
      "id", "1", "term_s", "YYYY", group, "1", "test_i", "5", "test_l", "10", "test_f", "2000"
    };
    assertU(adoc(doc));
    assertU(commit());
    String[] doc1 = {
      "id", "2", "term_s", "YYYY", group, "1", "test_i", "50", "test_l", "100", "test_f", "200"
    };
    assertU(adoc(doc1));

    String[] doc2 = {
      "id", "3", "term_s", "YYYY", "test_i", "5000", "test_l", "100", "test_f", "200"
    };
    assertU(adoc(doc2));
    assertU(commit());
    String[] doc3 = {
      "id", "4", "term_s", "YYYY", "test_i", "500", "test_l", "1000", "test_f", "2000"
    };
    assertU(adoc(doc3));

    String[] doc4 = {
      "id", "5", "term_s", "YYYY", group, "2", "test_i", "4", "test_l", "10", "test_f", "2000"
    };
    assertU(adoc(doc4));
    assertU(commit());
    String[] doc5 = {
      "id", "6", "term_s", "YYYY", group, "2", "test_i", "10", "test_l", "100", "test_f", "200"
    };
    assertU(adoc(doc5));
    assertU(commit());

    String[] doc6 = {
      "id", "7", "term_s", "YYYY", group, "1", "test_i", "8", "test_l", "50", "test_f", "300"
    };
    assertU(adoc(doc6));
    assertU(commit());

    // Test collapse by score and following sort by score
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + "" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    assertQ(
        req(params, "indent", "on"),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='2']",
        "//result/doc[2]/str[@name='id'][.='6']");

    // SOLR-5544 test ordering with empty sort param
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " nullPolicy=expand min=test_f" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    params.add("sort", "");
    assertQ(
        req(params),
        "*[count(//doc)=4]",
        "//result/doc[1]/str[@name='id'][.='3']",
        "//result/doc[2]/str[@name='id'][.='4']",
        "//result/doc[3]/str[@name='id'][.='2']",
        "//result/doc[4]/str[@name='id'][.='6']");

    // Test value source collapse criteria
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq", "{!collapse field=" + group + " nullPolicy=collapse min=field(test_i)" + hint + "}");
    params.add("sort", "test_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='4']",
        "//result/doc[2]/str[@name='id'][.='1']",
        "//result/doc[3]/str[@name='id'][.='5']");

    // Test value source collapse criteria with cscore function
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq", "{!collapse field=" + group + " nullPolicy=collapse min=cscore()" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='4']",
        "//result/doc[2]/str[@name='id'][.='1']",
        "//result/doc[3]/str[@name='id'][.='5']");

    // Test value source collapse criteria with cscore function but no top level score sort
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq", "{!collapse field=" + group + " nullPolicy=collapse min=cscore()" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    params.add("fl", "id");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='4']",
        "//result/doc[3]/str[@name='id'][.='1']");

    // Test value source collapse criteria with compound cscore function
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq",
        "{!collapse field="
            + group
            + " nullPolicy=collapse min=sum(cscore(),field(test_i))"
            + hint
            + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='4']",
        "//result/doc[2]/str[@name='id'][.='1']",
        "//result/doc[3]/str[@name='id'][.='5']");

    // Test collapse by score with elevation

    params = new ModifiableSolrParams();
    params.add("q", "YYYY");
    params.add("fq", "{!collapse field=" + group + " nullPolicy=collapse" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    params.add("qf", "term_s");
    params.add("qt", "/elevate");
    assertQ(
        req(params),
        "*[count(//doc)=4]",
        "//result/doc[1]/str[@name='id'][.='1']",
        "//result/doc[2]/str[@name='id'][.='2']",
        "//result/doc[3]/str[@name='id'][.='3']",
        "//result/doc[4]/str[@name='id'][.='6']");

    // Test SOLR-5773 with score collapse criteria
    // try both default & sort localparams as alternate ways to ask for max score
    for (String maxscore : new String[] {"  ", " sort='score desc' "}) {
      params = new ModifiableSolrParams();
      params.add("q", "YYYY");
      params.add(
          "fq", "{!collapse field=" + group + maxscore + " nullPolicy=collapse" + hint + "}");
      params.add("defType", "edismax");
      params.add("bf", "field(test_i)");
      params.add("qf", "term_s");
      params.add("qt", "/elevate");
      params.add("elevateIds", "1,5");
      assertQ(
          req(params),
          "*[count(//doc)=3]",
          "//result/doc[1]/str[@name='id'][.='1']",
          "//result/doc[2]/str[@name='id'][.='5']",
          "//result/doc[3]/str[@name='id'][.='3']");
    }

    // Test SOLR-5773 with max field collapse criteria
    // try both max & sort localparams as alternate ways to ask for max group head
    for (String max : new String[] {" max=test_i ", " sort='test_i desc' "}) {
      params = new ModifiableSolrParams();
      params.add("q", "YYYY");
      params.add("fq", "{!collapse field=" + group + max + "nullPolicy=collapse" + hint + "}");
      params.add("defType", "edismax");
      params.add("bf", "field(test_i)");
      params.add("qf", "term_s");
      params.add("qt", "/elevate");
      params.add("elevateIds", "1,5");
      assertQ(
          req(params),
          "*[count(//doc)=3]",
          "//result/doc[1]/str[@name='id'][.='1']",
          "//result/doc[2]/str[@name='id'][.='5']",
          "//result/doc[3]/str[@name='id'][.='3']");
    }

    // Test SOLR-5773 with min field collapse criteria
    // try both min & sort localparams as alternate ways to ask for min group head
    for (String min : new String[] {" min=test_i ", " sort='test_i asc' "}) {
      params = new ModifiableSolrParams();
      params.add("q", "YYYY");
      params.add("fq", "{!collapse field=" + group + min + "nullPolicy=collapse" + hint + "}");
      params.add("defType", "edismax");
      params.add("bf", "field(test_i)");
      params.add("qf", "term_s");
      params.add("qt", "/elevate");
      params.add("elevateIds", "1,5");
      assertQ(
          req(params),
          "*[count(//doc)=3]",
          "//result/doc[1]/str[@name='id'][.='1']",
          "//result/doc[2]/str[@name='id'][.='5']",
          "//result/doc[3]/str[@name='id'][.='4']");
    }

    // Test SOLR-5773 elevating documents with null group
    params = new ModifiableSolrParams();
    params.add("q", "YYYY");
    params.add("fq", "{!collapse field=" + group + "" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    params.add("qf", "term_s");
    params.add("qt", "/elevate");
    params.add("elevateIds", "3,4");
    assertQ(
        req(params),
        "*[count(//doc)=4]",
        "//result/doc[1]/str[@name='id'][.='3']",
        "//result/doc[2]/str[@name='id'][.='4']",
        "//result/doc[3]/str[@name='id'][.='2']",
        "//result/doc[4]/str[@name='id'][.='6']");

    // Non-trivial sort local param for picking group head
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq",
        "{!collapse field="
            + group
            + " nullPolicy=collapse sort='term_s asc, test_i asc' "
            + hint
            + "}");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='4']",
        "//result/doc[3]/str[@name='id'][.='1']");
    //
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add(
        "fq",
        "{!collapse field="
            + group
            + " nullPolicy=collapse sort='term_s asc, test_i desc' "
            + hint
            + "}");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='6']",
        "//result/doc[2]/str[@name='id'][.='3']",
        "//result/doc[3]/str[@name='id'][.='2']");

    // Test collapse by min int field and top level sort
    // try both min & sort localparams as alternate ways to ask for min group head
    for (String min : new String[] {" min=test_i ", " sort='test_i asc' "}) {
      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + min + hint + "}");
      params.add("sort", "id_i desc");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          "//result/doc[1]/str[@name='id'][.='5']",
          "//result/doc[2]/str[@name='id'][.='1']");

      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + min + hint + "}");
      params.add("sort", "id_i asc");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          "//result/doc[1]/str[@name='id'][.='1']",
          "//result/doc[2]/str[@name='id'][.='5']");

      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + min + hint + "}");
      params.add("sort", "test_l asc,id_i desc");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          "//result/doc[1]/str[@name='id'][.='5']",
          "//result/doc[2]/str[@name='id'][.='1']");

      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + min + hint + "}");
      params.add("sort", "score desc,id_i asc");
      params.add("defType", "edismax");
      params.add("bf", "field(id_i)");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          "//result/doc[1]/str[@name='id'][.='5']",
          "//result/doc[2]/str[@name='id'][.='1']");
    }

    // Test collapse by max int field
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " max=test_i" + hint + "}");
    params.add("sort", "test_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='6']",
        "//result/doc[2]/str[@name='id'][.='2']");

    try {
      // Test collapse by min long field
      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + " min=test_l" + hint + "}");
      params.add("sort", "test_i desc");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          "//result/doc[1]/str[@name='id'][.='1']",
          "//result/doc[2]/str[@name='id'][.='5']");

      // Test collapse by max long field
      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + " max=test_l" + hint + "}");
      params.add("sort", "test_i desc");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          "//result/doc[1]/str[@name='id'][.='2']",
          "//result/doc[2]/str[@name='id'][.='6']");
    } catch (Exception e) {
      if (!numeric) {
        throw e;
      }
    }

    // Test collapse by min float field
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " min=test_f" + hint + "}");
    params.add("sort", "test_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='2']",
        "//result/doc[2]/str[@name='id'][.='6']");

    // Test collapse by min float field
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " max=test_f" + hint + "}");
    params.add("sort", "test_i asc");
    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='1']");

    // Test collapse by min float field sort by score
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " max=test_f" + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(id_i)");
    params.add("fl", "score, id");
    params.add("facet", "true");
    params.add("fq", "{!tag=test}term_s:YYYY");
    params.add("facet.field", "{!ex=test}term_s");

    assertQ(
        req(params),
        "*[count(//doc)=2]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='1']");

    // Test collapse using selector field in no docs
    // tie selector in all of these cases
    for (String selector :
        new String[] {
          " min=bogus_i ", " sort='bogus_i asc' ",
          " max=bogus_i ", " sort='bogus_i desc' ",
          " min=bogus_tf ", " sort='bogus_tf asc' ",
          " max=bogus_tf ", " sort='bogus_tf desc' ",
          " sort='bogus_td asc' ", " sort='bogus_td desc' ",
          " sort='bogus_s asc' ", " sort='bogus_s desc' ",
        }) {
      params = new ModifiableSolrParams();
      params.add("q", "*:*");
      params.add("fq", "{!collapse field=" + group + selector + hint + "}");
      params.add("sort", group + " asc");
      assertQ(
          req(params),
          "*[count(//doc)=2]",
          // since selector is bogus, group head is undefined (should be index order, but don't make
          // absolute assumptions: segments may be re-ordered) key assertion is that there is one
          // doc from each group & groups are in order
          "//result/doc[1]/*[@name='" + group + "'][starts-with(.,'1')]",
          "//result/doc[2]/*[@name='" + group + "'][starts-with(.,'2')]");
    }

    // attempting to use cscore() in sort local param should fail
    assertQEx(
        "expected error trying to sort on a function that includes cscore()",
        req(
            params(
                "q", "{!func}sub(sub(test_l,1000),id_i)",
                "fq", "{!collapse field=" + group + " sort='abs(cscore()) asc, id_i asc'}",
                "sort", "score asc")),
        SolrException.ErrorCode.BAD_REQUEST);

    // multiple params for picking groupHead should all fail
    for (String bad :
        new String[] {
          "{!collapse field=" + group + " min=test_f max=test_f}",
          "{!collapse field=" + group + " min=test_f sort='test_f asc'}",
          "{!collapse field=" + group + " max=test_f sort='test_f asc'}"
        }) {
      assertQEx(
          "Expected error: " + bad,
          req(params("q", "*:*", "fq", bad)),
          SolrException.ErrorCode.BAD_REQUEST);
    }

    // multiple params for picking groupHead should work as long as only one is non-null
    // sort used
    for (SolrParams collapse :
        new SolrParams[] {
          // these should all be equally valid
          params(
              "fq",
              "{!collapse field=" + group + " nullPolicy=collapse sort='test_i asc'" + hint + "}"),
          params(
              "fq",
              "{!collapse field="
                  + group
                  + " nullPolicy=collapse min='' sort='test_i asc'"
                  + hint
                  + "}"),
          params(
              "fq",
              "{!collapse field="
                  + group
                  + " nullPolicy=collapse max='' sort='test_i asc'"
                  + hint
                  + "}"),
          params(
              "fq",
              "{!collapse field="
                  + group
                  + " nullPolicy=collapse min=$x sort='test_i asc'"
                  + hint
                  + "}"),
          params(
              "fq",
              "{!collapse field="
                  + group
                  + " nullPolicy=collapse min=$x sort='test_i asc'"
                  + hint
                  + "}",
              "x",
              ""),
        }) {

      assertQ(
          req(collapse, "q", "*:*", "sort", "test_i desc"),
          "*[count(//doc)=3]",
          "//result/doc[1]/str[@name='id'][.='4']",
          "//result/doc[2]/str[@name='id'][.='1']",
          "//result/doc[3]/str[@name='id'][.='5']");
    }

    // Test nullPolicy expand
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " max=test_f nullPolicy=expand" + hint + "}");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=4]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='4']",
        "//result/doc[3]/str[@name='id'][.='3']",
        "//result/doc[4]/str[@name='id'][.='1']");

    // Test nullPolicy collapse
    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " max=test_f nullPolicy=collapse" + hint + "}");
    params.add("sort", "id_i desc");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='5']",
        "//result/doc[2]/str[@name='id'][.='4']",
        "//result/doc[3]/str[@name='id'][.='1']");

    params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + hint + "}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    params.add("fq", "{!tag=test_i}id:5");
    params.add("facet", "true");
    params.add("facet.field", "{!ex=test_i}test_i");
    params.add("facet.mincount", "1");
    assertQ(
        req(params),
        "*[count(//doc)=1]",
        "*[count(//lst[@name='facet_fields']/lst[@name='test_i']/int)=2]");

    // SOLR-13970
    SolrException ex =
        expectThrows(
            SolrException.class,
            () -> {
              h.query(
                  req(
                      params(
                          "q",
                          "*:*",
                          "fq",
                          "{!collapse field=" + group + hint + "}",
                          "group",
                          "true",
                          "group.field",
                          "id")));
            });
    assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
    assertThat(ex.getMessage(), containsString("Can not use collapse with Grouping enabled"));

    // delete the elevated docs, confirm collapsing still works
    assertU(delI("1"));
    assertU(delI("2"));
    assertU(commit());
    params = new ModifiableSolrParams();
    params.add("q", "YYYY");
    params.add("fq", "{!collapse field=" + group + hint + " nullPolicy=collapse}");
    params.add("defType", "edismax");
    params.add("bf", "field(test_i)");
    params.add("qf", "term_s");
    params.add("qt", "/elevate");
    assertQ(
        req(params),
        "*[count(//doc)=3]",
        "//result/doc[1]/str[@name='id'][.='3']",
        "//result/doc[2]/str[@name='id'][.='6']",
        "//result/doc[3]/str[@name='id'][.='7']");
  }

  @Test
  public void testMissingFieldParam() {
    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse}");
    assertQEx(
        "It should respond with a bad request when the 'field' param is missing",
        req(params),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  @Test
  public void testEmptyCollection() {
    // group_s is docValues=false and group_dv_s is docValues=true
    String group = (random().nextBoolean() ? "group_s" : "group_s_dv");

    // min-or-max is for CollapsingScoreCollector vs. CollapsingFieldValueCollector
    String optional_min_or_max =
        (random().nextBoolean()
            ? ""
            : (random().nextBoolean() ? "min=field(test_i)" : "max=field(test_i)"));

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add("q", "*:*");
    params.add("fq", "{!collapse field=" + group + " " + optional_min_or_max + "}");
    assertQ(req(params), "*[count(//doc)=0]");

    // if a field is uninvertible=false, it should behave the same as a field that is indexed=false
    // this is currently ok on fields that don't exist on any docs in the index
    for (String f : Arrays.asList("not_indexed_sS", "indexed_s_not_uninvert")) {
      for (String hint : Arrays.asList("", " hint=top_fc")) {
        SolrException e =
            expectThrows(
                SolrException.class,
                () -> h.query(req("q", "*:*", "fq", "{!collapse field=" + f + hint + "}")));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
        assertTrue(
            "unexpected Message: " + e.getMessage(),
            e.getMessage()
                .contains(
                    "Collapsing field '"
                        + f
                        + "' "
                        + "should be either docValues enabled or indexed with uninvertible enabled"));
      }
    }
  }

  public void testNoDocsHaveGroupField() {
    // as unlikely as this test seems, it's important for the possibility that a segment exists w/o
    // any live docs that have DocValues for the group field -- ie: every doc in segment is in null
    // group.

    assertU(adoc("id", "1", "group_s", "group1", "test_i", "5", "test_l", "10"));
    assertU(commit());
    assertU(adoc("id", "2", "group_s", "group1", "test_i", "5", "test_l", "1000"));
    assertU(adoc("id", "3", "group_s", "group1", "test_i", "5", "test_l", "1000"));
    assertU(adoc("id", "4", "group_s", "group1", "test_i", "10", "test_l", "100"));
    //
    assertU(adoc("id", "5", "group_s", "group2", "test_i", "5", "test_l", "10", "term_s", "YYYY"));
    assertU(commit());
    assertU(adoc("id", "6", "group_s", "group2", "test_i", "5", "test_l", "1000"));
    assertU(
        adoc("id", "7", "group_s", "group2", "test_i", "5", "test_l", "1000", "term_s", "XXXX"));
    assertU(adoc("id", "8", "group_s", "group2", "test_i", "10", "test_l", "100"));
    assertU(commit());

    // none of these grouping fields are in any doc
    for (String group :
        new String[] {
          "field=bogus_s",
          "field=bogus_s_dv",
          "field=bogus_s hint=top_fc", // alternative docvalues codepath w/ hint
          "field=bogus_s_dv hint=top_fc", // alternative docvalues codepath w/ hint
          "field=bogus_i",
          "field=bogus_tf"
        }) {

      // for any of these selectors, behavior of these checks should be consistent
      for (String selector :
          new String[] {
            "",
            " sort='score desc' ",
            " min=test_i ",
            " max=test_i ",
            " sort='test_i asc' ",
            " sort='test_i desc' ",
            " min=test_f ",
            " max=test_f ",
            " sort='test_f asc' ",
            " sort='test_f desc' ",
            " sort='group_s asc' ",
            " sort='group_s desc' ",
            // fields that don't exist
            " min=bogus_sort_i ",
            " max=bogus_sort_i ",
            " sort='bogus_sort_i asc' ",
            " sort='bogus_sort_i desc' ",
            " sort='bogus_sort_s asc' ",
            " sort='bogus_sort_s desc' ",
          }) {

        ModifiableSolrParams params;

        // w/default nullPolicy, no groups found
        params = new ModifiableSolrParams();
        params.add("q", "*:*");
        params.add("sort", "id_i desc");
        params.add("fq", "{!collapse " + group + " " + selector + "}");
        assertQ(req(params), "*[count(//doc)=0]");

        // w/nullPolicy=expand, every doc found
        params = new ModifiableSolrParams();
        params.add("q", "*:*");
        params.add("sort", "id_i desc");
        params.add("fq", "{!collapse field=" + group + " nullPolicy=expand " + selector + "}");
        assertQ(
            req(params),
            "*[count(//doc)=8]",
            "//result/doc[1]/str[@name='id'][.='8']",
            "//result/doc[2]/str[@name='id'][.='7']",
            "//result/doc[3]/str[@name='id'][.='6']",
            "//result/doc[4]/str[@name='id'][.='5']",
            "//result/doc[5]/str[@name='id'][.='4']",
            "//result/doc[6]/str[@name='id'][.='3']",
            "//result/doc[7]/str[@name='id'][.='2']",
            "//result/doc[8]/str[@name='id'][.='1']");
      }
    }
  }

  public void testGroupHeadSelector() {
    GroupHeadSelector s;

    expectThrows(
        SolrException.class,
        "no exception with multi criteria",
        () -> GroupHeadSelector.build(params("sort", "foo_s asc", "min", "bar_s")));

    s = GroupHeadSelector.build(params("min", "foo_s"));
    assertEquals(GroupHeadSelectorType.MIN, s.type);
    assertEquals("foo_s", s.selectorText);

    s = GroupHeadSelector.build(params("max", "foo_s"));
    assertEquals(GroupHeadSelectorType.MAX, s.type);
    assertEquals("foo_s", s.selectorText);
    assertNotEquals(s, GroupHeadSelector.build(params("min", "foo_s", "other", "stuff")));

    s = GroupHeadSelector.build(params());
    assertEquals(GroupHeadSelectorType.SCORE, s.type);
    assertNotNull(s.selectorText);
    assertEquals(GroupHeadSelector.build(params()), s);
    assertNotEquals(s, GroupHeadSelector.build(params("min", "BAR_s")));

    s = GroupHeadSelector.build(params("sort", "foo_s asc"));
    assertEquals(GroupHeadSelectorType.SORT, s.type);
    assertEquals("foo_s asc", s.selectorText);
    assertEquals(GroupHeadSelector.build(params("sort", "foo_s asc")), s);
    assertNotEquals(s, GroupHeadSelector.build(params("sort", "BAR_s asc")));
    assertNotEquals(s, GroupHeadSelector.build(params("min", "BAR_s")));
    assertNotEquals(s, GroupHeadSelector.build(params()));

    assertEquals(
        GroupHeadSelector.build(params("sort", "foo_s asc")).hashCode(),
        GroupHeadSelector.build(
                params(
                    "sort", "foo_s asc",
                    "other", "stuff"))
            .hashCode());
  }

  @Test
  public void testForNotSupportedCases() {
    String[] doc = {
      "id",
      "3",
      "term_s",
      "YYYY",
      "test_ii",
      "5000",
      "test_l",
      "100",
      "test_f",
      "200",
      "not_indexed_sS",
      "zzz",
      "indexed_s_not_uninvert",
      "zzz"
    };
    assertU(adoc(doc));
    assertU(commit());

    // collapsing on multivalued field
    assertQEx(
        "Should Fail with Bad Request",
        "Collapsing not supported on multivalued fields",
        req("q", "*:*", "fq", "{!collapse field=test_ii}"),
        SolrException.ErrorCode.BAD_REQUEST);

    // collapsing on unknown field
    assertQEx(
        "Should Fail with Bad Request",
        "org.apache.solr.search.SyntaxError: undefined field: \"bleh\"",
        req("q", "*:*", "fq", "{!collapse field=bleh}"),
        SolrException.ErrorCode.BAD_REQUEST);

    // if a field is uninvertible=false, it should behave the same as a field that is indexed=false
    // this also tests docValues=false along with indexed=false or univertible=false
    for (String f : Arrays.asList("not_indexed_sS", "indexed_s_not_uninvert")) {
      {
        SolrException e =
            expectThrows(
                SolrException.class,
                () -> h.query(req(params("q", "*:*", "fq", "{!collapse field=" + f + "}"))));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
        assertTrue(
            "unexpected Message: " + e.getMessage(),
            e.getMessage()
                .contains(
                    "Collapsing field '"
                        + f
                        + "' "
                        + "should be either docValues enabled or indexed with uninvertible enabled"));
      }
      {
        SolrException e =
            expectThrows(
                SolrException.class,
                () -> h.query(req("q", "*:*", "fq", "{!collapse field=" + f + " hint=top_fc}")));
        assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
        assertTrue(
            "unexpected Message: " + e.getMessage(),
            e.getMessage()
                .contains(
                    "Collapsing field '"
                        + f
                        + "' "
                        + "should be either docValues enabled or indexed with uninvertible enabled"));
      }
    }
  }

  @Test
  public void test64BitCollapseFieldException() {
    assertQEx(
        "Should Fail For collapsing on Long fields",
        "Collapsing field should be of either String, Int or Float type",
        req("q", "*:*", "fq", "{!collapse field=group_l}"),
        SolrException.ErrorCode.BAD_REQUEST);

    assertQEx(
        "Should Fail For collapsing on Double fields",
        "Collapsing field should be of either String, Int or Float type",
        req("q", "*:*", "fq", "{!collapse field=group_d}"),
        SolrException.ErrorCode.BAD_REQUEST);

    assertQEx(
        "Should Fail For collapsing on Date fields",
        "Collapsing field should be of either String, Int or Float type",
        req("q", "*:*", "fq", "{!collapse field=group_dt}"),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  @Test
  public void testMinExactCountDisabledByCollapse() {
    int numDocs = 10;
    String collapseFieldInt = "field_ti_dv";
    String collapseFieldFloat = "field_tf_dv";
    String collapseFieldString = "field_s_dv";
    for (int i = 0; i < numDocs; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              "field_s",
              String.valueOf(i % 2),
              collapseFieldInt,
              String.valueOf(i),
              collapseFieldFloat,
              String.valueOf(i),
              collapseFieldString,
              String.valueOf(i)));
      assertU(commit());
    }

    for (String collapseField :
        Arrays.asList(collapseFieldInt, collapseFieldFloat, collapseFieldString)) {
      // all of our docs have a value in the collapse field(s) so the policy shouldn't matter...
      for (String policy :
          Arrays.asList("", " nullPolicy=ignore", " nullPolicy=expand", " nullPolicy=collapse")) {
        assertQ(
            req(
                "q", "{!cache=false}field_s:1",
                "rows", "1",
                "minExactCount", "1", // collapse should force this to be ignored
                // this collapse will end up creating a group for each matched doc
                "fq", "{!collapse field=" + collapseField + policy + "}"),
            "//*[@numFoundExact='true']",
            "//*[@numFound='" + (numDocs / 2) + "']");
      }
    }
  }

  public void testNullGroupNumericVsStringCollapse() {
    // NOTE: group_i and group_s will contain identical content so these need to be "numbers"...
    // The specific numbers shouldn't matter (and we explicitly test '0' to confirm legacy
    // bug/behavior of treating 0 as null is no longer a problem) ...
    final String A = "-1";
    final String B = "0";
    final String C = "1";

    // Stub out our documents.  From now on assume highest "id" of each group should be group
    // head...
    final List<SolrInputDocument> docs =
        sdocs(
            sdoc("id", "0"), // null group
            sdoc("id", "1", "group_i", A, "group_s", A),
            sdoc("id", "2", "group_i", B, "group_s", B),
            sdoc("id", "3", "group_i", B, "group_s", B), // B head
            sdoc("id", "4"), // null group
            sdoc("id", "5", "group_i", A, "group_s", A),
            sdoc("id", "6", "group_i", C, "group_s", C),
            sdoc("id", "7"), // null group                 // null head
            sdoc("id", "8", "group_i", A, "group_s", A), // A head
            sdoc("id", "9", "group_i", C, "group_s", C)); // C head

    final List<String> SELECTOR_FIELD_SUFFIXES = Arrays.asList("_i", "_l", "_f");
    // add all the fields we'll be using as group head selectors...
    int asc = 0;
    int desc = 0;
    for (SolrInputDocument doc : docs) {
      for (String type : SELECTOR_FIELD_SUFFIXES) {
        doc.setField("asc" + type, asc);
        doc.setField("desc" + type, desc);
      }
      asc++;
      desc--;
    }

    // convert our docs to update commands, along with some commits, in a shuffled order and process
    // all of them...
    final List<String> updates =
        Stream.concat(Stream.of(commit(), commit()), docs.stream().map(doc -> adoc(doc)))
            .collect(Collectors.toList());
    Collections.shuffle(updates, random());
    for (String u : updates) {
      assertU(u);
    }
    assertU(commit());

    // function based query for deterministic scores
    final String q = "{!func}sum(asc_i,42)";

    // results should be the same regardless of whether we collapse on a string field or numeric
    // field (docs have identical group identifiers in both fields)
    for (String f : Arrays.asList("group_i", "group_s")) {

      // these group head selectors should all result in identical group heads for our query...
      for (String suffix : SELECTOR_FIELD_SUFFIXES) {

        for (String selector :
            Arrays.asList(
                "",
                "max=asc" + suffix,
                "min=desc" + suffix,
                "sort='asc" + suffix + " desc'",
                "sort='desc" + suffix + " asc'",
                "max=sum(42,asc" + suffix + ")",
                "min=sum(42,desc" + suffix + ")",
                "max=sub(0,desc" + suffix + ")",
                "min=sub(0,asc" + suffix + ")")) {

          if (selector.endsWith("_l") && f.endsWith("_i")) {
            assertQEx(
                "expected known limitation of using long for min/max selector when doing numeric collapse",
                "min/max must be Int or Float",
                req("q", q, "fq", "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}"),
                SolrException.ErrorCode.BAD_REQUEST);

            continue;
          }

          // ignore nulls
          assertQ(
              req(
                  params(
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}")),
              "*[count(//doc)=3]",
              "//result/doc[1]/str[@name='id'][.='9']", // group C
              "//result/doc[2]/str[@name='id'][.='8']", // group A
              "//result/doc[3]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "1,5",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}")),
              "*[count(//doc)=4]",
              "//result/doc[1]/str[@name='id'][.='1']", // elevated, prevents group A
              "//result/doc[2]/str[@name='id'][.='5']", // elevated, (also) prevents group A
              "//result/doc[3]/str[@name='id'][.='9']", // group C
              "//result/doc[4]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "0,7",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}")),
              "*[count(//doc)=5]",
              "//result/doc[1]/str[@name='id'][.='0']", // elevated (null)
              "//result/doc[2]/str[@name='id'][.='7']", // elevated (null)
              "//result/doc[3]/str[@name='id'][.='9']", // group C
              "//result/doc[4]/str[@name='id'][.='8']", // group A
              "//result/doc[5]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "6,0",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=ignore " + selector + "}")),
              "*[count(//doc)=4]",
              "//result/doc[1]/str[@name='id'][.='6']", // elevated, prevents group C
              "//result/doc[2]/str[@name='id'][.='0']", // elevated (null)
              "//result/doc[3]/str[@name='id'][.='8']", // group A
              "//result/doc[4]/str[@name='id'][.='3']" // group B
              );

          // collapse nulls
          assertQ(
              req(
                  params(
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}")),
              "*[count(//doc)=4]",
              "//result/doc[1]/str[@name='id'][.='9']", // group C
              "//result/doc[2]/str[@name='id'][.='8']", // group A
              "//result/doc[3]/str[@name='id'][.='7']", // group null
              "//result/doc[4]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "1,5",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}")),
              "*[count(//doc)=5]",
              "//result/doc[1]/str[@name='id'][.='1']", // elevated, prevents group A
              "//result/doc[2]/str[@name='id'][.='5']", // elevated, (also) prevents group A
              "//result/doc[3]/str[@name='id'][.='9']", // group C
              "//result/doc[4]/str[@name='id'][.='7']", // group null
              "//result/doc[5]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "0,7",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}")),
              "*[count(//doc)=5]",
              "//result/doc[1]/str[@name='id'][.='0']", // elevated (null)
              "//result/doc[2]/str[@name='id'][.='7']", // elevated (null)
              "//result/doc[3]/str[@name='id'][.='9']", // group C
              "//result/doc[4]/str[@name='id'][.='8']", // group A
              "//result/doc[5]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "6,0",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=collapse " + selector + "}")),
              "*[count(//doc)=4]",
              "//result/doc[1]/str[@name='id'][.='6']", // elevated, prevents group C
              "//result/doc[2]/str[@name='id'][.='0']", // elevated (null)
              "//result/doc[3]/str[@name='id'][.='8']", // group A
              "//result/doc[4]/str[@name='id'][.='3']" // group B
              );

          // expand nulls
          assertQ(
              req(
                  params(
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=expand " + selector + "}")),
              "*[count(//doc)=6]",
              "//result/doc[1]/str[@name='id'][.='9']", // group C
              "//result/doc[2]/str[@name='id'][.='8']", // group A
              "//result/doc[3]/str[@name='id'][.='7']", // null
              "//result/doc[4]/str[@name='id'][.='4']", // null
              "//result/doc[5]/str[@name='id'][.='3']", // group B
              "//result/doc[6]/str[@name='id'][.='0']" // null
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "1,5",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=expand " + selector + "}")),
              "*[count(//doc)=7]",
              "//result/doc[1]/str[@name='id'][.='1']", // elevated, prevents group A
              "//result/doc[2]/str[@name='id'][.='5']", // elevated, (also) prevents group A
              "//result/doc[3]/str[@name='id'][.='9']", // group C
              "//result/doc[4]/str[@name='id'][.='7']", // null
              "//result/doc[5]/str[@name='id'][.='4']", // null
              "//result/doc[6]/str[@name='id'][.='3']", // group B
              "//result/doc[7]/str[@name='id'][.='0']" // null
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "0,7",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=expand " + selector + "}")),
              "*[count(//doc)=6]",
              "//result/doc[1]/str[@name='id'][.='0']", // elevated (null)
              "//result/doc[2]/str[@name='id'][.='7']", // elevated (null)
              "//result/doc[3]/str[@name='id'][.='9']", // group C
              "//result/doc[4]/str[@name='id'][.='8']", // group A
              "//result/doc[5]/str[@name='id'][.='4']", // null
              "//result/doc[6]/str[@name='id'][.='3']" // group B
              );
          assertQ(
              req(
                  params(
                      "qt",
                      "/elevate",
                      "elevateIds",
                      "6,0",
                      "q",
                      q,
                      "fq",
                      "{!collapse field=" + f + " nullPolicy=expand " + selector + "}")),
              "*[count(//doc)=6]",
              "//result/doc[1]/str[@name='id'][.='6']", // elevated, prevents group C
              "//result/doc[2]/str[@name='id'][.='0']", // elevated (null)
              "//result/doc[3]/str[@name='id'][.='8']", // group A
              "//result/doc[4]/str[@name='id'][.='7']", // null
              "//result/doc[5]/str[@name='id'][.='4']", // null
              "//result/doc[6]/str[@name='id'][.='3']" // group B
              );
        }
      }
    }
  }
}
